﻿using System.Collections.Generic;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Web;
using System;

namespace ZhuGe.Common
{
    /// <summary>
    /// Responds to a request for javascript files.
    /// 
    /// Use in markup like:
    /// <script type="text/javascript" src="JavascriptLoader.ashx?~/scripts/effect_*.js&~/scripts/AccessibilityTools.js"></script>
    /// 
    /// </summary><remarks>
    /// Each script after the '?' marker in the URL will be added in order.
    /// The list is '&' seperated. You can use the kleene star ('*') to select multiple files,
    /// but no multiple directories. You may use the '~/' site root prefix.
    /// </remarks>
    public class JavascriptLoader : IHttpHandler
    {
        private HttpContext Context;
        HttpResponse Response;
        HttpRequest Request;
        HttpServerUtility Server;
        private StringBuilder Builder;

        public void ProcessRequest(HttpContext context)
        {
            #region Setup
            // Link up the names you're used to
            Response = context.Response;
            Request = context.Request;
            Server = context.Server;
            Context = context;
            // prepare the outgoing page:
            Response.ClearContent();
            Response.ClearHeaders();

            // Check for cache-control:
            if (context.Request.IsLocal)
            { // don't cache local requests
                context.Response.AddHeader("Expires", DateTime.Now.ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
                context.Response.AddHeader("Cache-Control", "no-cache");
                context.Response.AddHeader("Last-Modified", DateTime.Now.ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
            }
            else
            {
                if (!string.IsNullOrEmpty(Request.Headers["If-Modified-Since"]))
                {
                    string cc = Request.Headers["cache-request-directive"];
                    if (cc != "no-cache" && cc != "no-store")
                    {
                        // Assume no modifications (this responder is for production code!)
                        context.Response.AddHeader("Expires", DateTime.Now.AddYears(5).ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
                        Response.StatusCode = 304; // not changed
                        Response.End();
                        return;
                    }
                }
                context.Response.AddHeader("Expires", DateTime.Now.AddYears(5).ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
                context.Response.AddHeader("Cache-Control", "public");
                context.Response.AddHeader("Last-Modified", DateTime.Now.AddDays(-30).ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
            }

            Response.ContentType = "text/javascript";
            #endregion

            CompressResponse(); // compress output if possible.

            var urls = new List<string>();
            urls.AddRange(Request.QueryString.ToString().Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries));
            foreach (string url in urls) { if (!url.StartsWith("version")) AddRequest(url); }

            CompressAndWriteScripts();
            Response.StatusCode = 200;
        }

        private const string GZIP = "gzip";
        private const string DEFLATE = "deflate";

        private void CompressResponse()
        {
            var context = HttpContext.Current;
            try
            {
                if (context.Request.Headers["Accept-encoding"] != null && context.Request.Headers["Accept-encoding"].Contains(DEFLATE)
                || context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"] != null && context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"].Contains(DEFLATE))
                { // prefer Deflate. It doesn't require checksums, and is therefore smaller and faster.
                    Context.Response.Filter = new DeflateStream(Context.Response.Filter, CompressionMode.Compress);
                    Context.Response.AppendHeader("Content-encoding", DEFLATE);
                }
                else if (context.Request.Headers["Accept-encoding"] != null && context.Request.Headers["Accept-encoding"].Contains(GZIP)
              || context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"] != null && context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"].Contains(GZIP))
                {
                    Context.Response.Filter = new GZipStream(Context.Response.Filter, CompressionMode.Compress);
                    Context.Response.AppendHeader("Content-encoding", GZIP);
                }
            }
            catch { }
        }

        /// <summary>
        /// Looks for files with a given pattern, then adds the script if found.
        /// You can use the kleene star ('*') to select multiple files.
        /// </summary>
        /// <param name="pattern"></param>
        private void AddRequest(string pattern)
        {
            if (Builder == null) Builder = new StringBuilder();
            if (string.IsNullOrEmpty(pattern)) return;

            string safe_uri = Server.UrlDecode(pattern).Replace("*", "%2a");

            string path;
            try
            {
                path = Server.MapPath(safe_uri);
            }
            catch
            {
                // Unmappable path (usually weird requests)
                Builder.AppendLine("/* Failed to map path: \"" + (safe_uri ?? "[null]") + "\" */");
                return; // don't bother with this file.
            }

            string root = Path.GetDirectoryName(path);
            string search = Server.UrlDecode(Path.GetFileName(path));


            string[] matches;
            try
            {
                matches = Directory.GetFiles(root, search, SearchOption.TopDirectoryOnly);
            }
            catch
            {
                Builder.AppendLine("/* Failed to access files for path: \"" + (safe_uri ?? "[null]") + "\" */");
                return; // don't bother with this file set.
            }

            foreach (string match in matches)
            {
                AddScript(match);
            }
        }

        /// <summary>
        /// Adds a script file to those waiting to be sent
        /// </summary>
        private void AddScript(string path)
        {
            if (Builder == null) Builder = new StringBuilder();
            if (string.IsNullOrEmpty(path)) return;
            if (!File.Exists(path)) return;

            Builder.AppendLine(File.ReadAllText(path));
            Builder.AppendLine();
        }

        private void CompressAndWriteScripts()
        {
            if (Builder == null) return;
            Response.Write(Builder.ToString());
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
